﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SimpleCsv
{
    public class CsvReader : IDisposable
    {
        readonly char DelimiterChar;
        readonly char QuoteChar;
        internal static char[] TrimChars = new char[] { ' ', '\t', '\r', '\n' };

        TextReader _reader;
        bool _leaveOpen;

        public CsvReader(TextReader reader, 
            char delimiterChar = ',', char quoteChar = '"', bool leaveOpen = false)
        {
            if (reader == null) { throw new ArgumentException("reader"); }

            _reader = reader;
            _leaveOpen = leaveOpen;

            DelimiterChar = delimiterChar;
            QuoteChar = quoteChar;
        }

        /// <summary>
        /// Read next row, null when done.
        /// </summary>
        /// <returns></returns>
        public CsvRow ReadRow()
        {
            if (_reader.Peek() == -1) { return null; }
            CsvRow rv = DecodeNextRow();
            return rv;
        }

        /// <summary>
        /// Decode a CSV line returning a set of CSV fields.
        /// Returns null if the string is empty.
        /// </summary>
        /// <param name="s"></param>
        /// <returns></returns>
        public static CsvRow DecodeRow(String s, 
            char delimiterChar = ',', char quoteChar = '"')
        {
            var tr = new StringReader(s);
            using(var r = new CsvReader(tr, delimiterChar, quoteChar))
            {
                return r.DecodeNextRow();
            }
        }

        CsvRow DecodeNextRow()
        {
            CsvRow fields = new CsvRow();
            StringBuilder csvField = new StringBuilder();

            while (true)
            {
                bool lineEnded = false;
                String field = ReadNextField(out lineEnded);

                // Empty line is treated as a CsvRow with one empty field,
                // not as a CsvRow with zero fields.
                fields.Add(field);

                if (lineEnded) { return fields; }
            }
        }

        #region State machine for parsing rows

        String ReadNextField(out bool endOfLine)
        {
            endOfLine = false;
            StringBuilder sb = new StringBuilder();
            ParseState state = ParseState.Start;

            while (true)
            {
                // EOF, build last field
                int nextCh = _reader.Read();
                if (nextCh == -1)
                {
                    endOfLine = true;
                    break;
                }

                char c = (char)nextCh;
                state = GetNextState(state, c);

                if (state == ParseState.EndOfField) { break; }
                if (state == ParseState.EndOfLine)
                {
                    endOfLine = true;
                    break;
                }

                sb.Append(c);
            }
            return DecodeCsvField(sb.ToString());
        }

        bool IsTrimChar(char c)
        {
            return Array.IndexOf(TrimChars, c) != -1;
        }

        enum ParseState
        {
            Start,
            PreOpen,
            Quoted,
            PreClosed,
            Unquoted, // normal state

            EndOfField,
            EndOfLine
        }

        ParseState GetNextState(ParseState ps, char c)
        {
            switch (ps)
            {
                case ParseState.Start:
                    if (c == '\n') { return ParseState.EndOfLine; }
                    if (c == DelimiterChar) { return ParseState.EndOfField; }

                    if (IsTrimChar(c)) { return ParseState.Start; }
                    if (c == QuoteChar) { return ParseState.PreOpen; }
                    return ParseState.Unquoted;

                case ParseState.Unquoted:
                    if (c == '\n') { return ParseState.EndOfLine; }
                    if (c == DelimiterChar) { return ParseState.EndOfField; }

                    return ParseState.Unquoted;

                case ParseState.PreOpen:
                    if (c == QuoteChar) { return ParseState.Unquoted; }
                    return ParseState.Quoted;

                case ParseState.Quoted:
                    if (c == QuoteChar) { return ParseState.PreClosed; }
                    return ParseState.Quoted;

                case ParseState.PreClosed:
                    if (c == '\n') { return ParseState.EndOfLine; }
                    if (c == DelimiterChar) { return ParseState.EndOfField; }

                    if (c == QuoteChar) { return ParseState.Quoted; }
                    return ParseState.Unquoted;
            }

            throw new ArgumentException("Invalid state: " + ps);
        }

        #endregion

        /// <summary>
        /// Decodes a single CSV field
        /// </summary>
        /// <param name="rawField"></param>
        /// <returns></returns>
        String DecodeCsvField(String rawField)
        {
            // Ignore leading and trailing whitespace adjacent to comma field separators are ignored.
            rawField = rawField.Trim(TrimChars);

            // Special case -- special chars present            
            if (rawField.Length >= 2 &&
                rawField.StartsWith("" + QuoteChar) &&
                rawField.EndsWith("" + QuoteChar))
            {
                rawField = rawField.Substring(1, rawField.Length - 2);
            }

            // Unescape double quotes
            rawField = rawField.Replace(
                "" + QuoteChar + QuoteChar,
                "" + QuoteChar);

            return rawField;
        }


        public void Dispose()
        {
            if (!_leaveOpen && _reader != null)
            {
                _reader.Dispose();
            }
        }
    }

}
